Using the SHBrowseForFolder API function

Last Updated: 20th May

The SHBrowseForFolder API function brings up a dialog window which allows the user to select a shell folder, which can include directories, printers, networks etc etc. Not only is it powerful, but it's also a pretty looking thing, utilising a treeview control to enumerate the shell folders. One of the key things to note about this function and other shell functions is the way that a folder is identified. Each shell folder has an item identifier, which is unique among items within its parent folder. Since all of its ancestor folders (parent, grandparent, etc etc.) have their own item ID's, a shell folder is thus uniquely identified by a list of item identifiers, which is called an item ID list.

Because shell folders can be many things, they are implemented as COM objects which through the IShellFolder interface can carry out various actions (the interface's methods). One of these actions is to convert an item ID list into a directory name (technically, namespace). According to the Win32API docs, whenever an object's IShellFolder interface is obtained, the interface must eventually be freed by calling its Release method. The neat thing is that Delphi 3.x does this for you! On the other hand, any item ID lists you create must be freed by you, via the shell allocator, which is an IMalloc interface. A call to the SHGetMalloc function will retrieve a pointer to the shell allocator.

So without further ado, here is a function which wraps up the SHBrowseForFolder API function, including copious amounts of comments which describe what the hell is going on:



function BrowseDirectoryMP(Handle : HWnd; InitialDir : String) : String;
{Handle is the parent of the BrowseDirectory dialog window, while
 InitialDir is the directory which is selected when the dialog window
 is opened.}
var
  MyShellMalloc : IMalloc;     // IMalloc task allocator
  MyBrowseInfo : TBrowseInfo;
  DestDir : String;
  shBuff : PChar;
  MyItemIDList, TheItemIDList, SelectionIDList : PItemIDList;
  ShellFolder: IShellFolder;          // the powerful IShellFolder interface
  OLEStr: array[0..MAX_PATH] of TOLEChar;    // need to use OLE strings
  Eaten, Attr: ULONG;
  Success : Boolean;
begin
    {This call is necessary as it retrieves a pointer to the IMalloc interface
    of the Win95 shell, which in turn is used to free memory that was allocated
    by the shell; something that certain shell functions require.}

    SHGetMalloc(MyShellMalloc);

    {Initialise destination string to be empty and set its length to MAX_PATH}

    DestDir := '';
    SetLength(DestDir, MAX_PATH);

    {The pszDisplayName member of the TSHBrowseInfo structure needs to be
    allocated space via the shell's IMalloc interface}

    shBuff := PChar(MyShellMalloc.Alloc(MAX_PATH));

    {if allocation is sucessful then proceed}

    if assigned(shBuff) then begin
      try

        {Retrieve a pointer to an item identifier list specifying the location
        of the My Computer virtual folder relative to the desktop folder}

        SHGetSpecialFolderLocation(Handle, CSIDL_DRIVES, MyItemIDList);
        try

          {MyItemIDList will be the root folder for the Browse Directory
          dialog. But what if you want a directory to be initially selected?
          Well, you first need to retrieve its item identifier. We do this
          via the IShellFolder interface for the desktop folder}

          if SHGetDesktopFolder(ShellFolder) = NO_ERROR then begin

            {The ParseDisplayName method will do the trick. But we need to
            make sure our directory string is a null-terminated Unicode string
            We use the StringToWideChar function to convert it properly}

            if ShellFolder.ParseDisplayName(Handle, nil,
                 StringToWideChar(InitialDir, OLEStr, MAX_PATH), Eaten,
                 SelectionIDList, Attr) = NO_ERROR then
              Success := True
            else
              Success := False;
          end else
            Success := False;

          {Before calling the function which brings up the dialog editor,
          we need to fill in the members of the relevant structure TBrowseInfo}

          try
            with MyBrowseInfo do begin
              hwndOwner := Handle;        // owner of dialog window
              pidlRoot := MyItemIDList;   // specified the root folder
              pszDisplayName := shBuff;   // this receives the selected folder
              lpszTitle := PChar('Please Select Directory'); // dialog title
              ulFlags := BIF_RETURNONLYFSDIRS; // return only file-system dirs
              if Success then begin

                {OK, this is the second part of selecting the initial directory
                of the browse dialog. lpfn points to a callback function, which
                the dialog window calls whenever events occur. One of the
                events is the opening of the dialog window. When this occurs
                we will send a selection message to browse dialog window,
                which will select the intial directory of our choice}

                lpfn := BrowseCallbackProc;

                {lParam gets passed to the callback function. It represents
                the item identifier (obtained above) of the directory we
                wish to select}

                lParam := Integer(SelectionIDList);
              end else begin

                {If, for whatever reason we couldn't obtain item identifier
                we set the callback function to nil}

                lpfn := nil;
                lParam := 0;
              end;
            end;

            {Here is where the dialog is called up for display, finally!
            The return result is the item identifier for the directory
            which was chosen by the user}

            TheItemIDList := SHBrowseForFolder(MyBrowseInfo);
            try

              {Convert the item identfier into a directory name}

              if SHGetPathFromIDList(TheItemIDList, shBuff) then
                DestDir := shBuff;
            finally

              {It is very important that free the item identfiers}

              MyShellMalloc.Free(TheItemIDList); // Clean-up
            end;
          finally
            MyShellMalloc.Free(SelectionIDList); // Clean-up.
          end;
        finally
          MyShellMalloc.Free(MyItemIDList); // Clean-up.
        end;
      finally
        MyShellMalloc.Free(shBuff); // Clean-up.
      end;
    end;
    Result := String(PChar(DestDir));  // the result!!
end;
And here is the callback function:


function BrowseCallbackProc(Wnd: HWnd; Msg: UINT; lParam: LPARAM;
                                   lData: LPARAM): integer; stdcall;
begin
  Result := 0;
  {upon startup, set the selection to the intial directory desired}
  if Msg = BFFM_INITIALIZED then begin
    SendMessage(Wnd, BFFM_SETSELECTION, WPARAM(False), lData);
  end; {if}
end;
lonewolf@tig.com.au